import { createSubscription } from "../utils/Subscription"
import DebugTilesRenderer from "../renderer/DebugTilesRenderer"
import TilesRenderer from "../renderer/TilesRenderer"
import R from "../other/321-19131"
import * as THREE from "three"
import configConst from "../constant/config.const"
import Logger from "../utils/Logger"
import p from "../other/543-91283"
import RoomValueMap from "../three/RoomValueMap"
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader"
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"
import RenderLayers from "../utils/RenderLayers"
import MeshBVH from "../mesh/MeshBVH"
import RoomMesh from "../mesh/RoomMesh"
import g from "../other/383-59855"
const y = {
  FLOAT: 5126,
  FLOAT_MAT3: 35675,
  FLOAT_MAT4: 35676,
  FLOAT_VEC2: 35664,
  FLOAT_VEC3: 35665,
  FLOAT_VEC4: 35666,
  LINEAR: 9729,
  REPEAT: 10497,
  SAMPLER_2D: 35678,
  POINTS: 0,
  LINES: 1,
  LINE_LOOP: 2,
  LINE_STRIP: 3,
  TRIANGLES: 4,
  TRIANGLE_STRIP: 5,
  TRIANGLE_FAN: 6,
  UNSIGNED_BYTE: 5121,
  UNSIGNED_SHORT: 5123
}
function E(e, t) {
  void 0 !== t.extras && "object" == typeof t.extras && Object.assign(e.userData, t.extras)
}
const L = new RoomValueMap<any>()
const _ = new Logger("tiled-mesh")
const b = new Map()
const A = p.tE
let D
let I
function C(e, t) {
  if (I) {
    return I
  } else {
    I = new KTX2Loader()
    I.setTranscoderPath("libs/three@0.128.0/basis/")
    I.detectSupport(e)
    I.basisLoader._initTranscoder()
    I.manager = t
    return I
  }
}
class MTTRMeshBvh {
  parser: any
  name: string
  constructor(e) {
    ;(this.parser = e), (this.name = "MTTR_three_mesh_bvh")
  }
  async loadMesh(e) {
    const t = async (t, s) => {
        var i, r
        const o = this.parser.json.meshes[e].primitives[s]
        if (null === (i = o.extensions) || void 0 === i ? void 0 : i.MTTR_three_mesh_bvh) {
          const e = null === (r = o.extensions) || void 0 === r ? void 0 : r.MTTR_three_mesh_bvh,
            s = {
              roots: (await Promise.all(e.roots.map(e => this.parser.loadAccessor(e)))).map((e: any) => e.array.buffer),
              index: void 0
            }
          t.geometry.boundsTree = MeshBVH.deserialize(s, t.geometry, {
            setIndex: !1
          })
        }
      },
      s = [],
      i = await this.loadMeshInternal(e)
    if (i)
      if ("Group" === i.type) {
        const e = i
        s.push(...e.children.map((e, s) => t(e, s)))
      } else "Mesh" === i.type && s.push(t(i, 0))
    return await Promise.all(s), i
  }
  async loadMeshInternal(e) {
    const t = this.parser,
      s = t,
      i = this.parser.json.meshes[e],
      r = i.primitives,
      o = []
    for (let e = 0, i = r.length; e < i; e++) {
      const i = void 0 === r[e].material ? t.createDefaultMaterial(s.cache) : t.getDependency("material", r[e].material)
      o.push(i)
    }
    return (
      o.push(t.loadGeometries(r)),
      Promise.all(o).then(function (s) {
        const o = s.slice(0, s.length - 1),
          n = s[s.length - 1],
          d = []
        for (let s = 0, l = n.length; s < l; s++) {
          const l = n[s],
            h = r[s]
          let u
          const c = o[s]
          if (h.mode !== y.TRIANGLES && void 0 !== h.mode) throw new Error("THREE.GLTFLoader: Primitive mode unsupported: " + h.mode)
          ;(u = new RoomMesh(0, 0, RenderLayers.DEFAULT)),
            (u.geometry = l),
            (u.material = c),
            (u.name = t.createUniqueName(i.name || "mesh_" + e)),
            E(u, i),
            t.assignFinalMaterial(u),
            d.push(u)
        }
        if (1 === d.length) return d[0]
        const l = new THREE.Group()
        for (let e = 0, t = d.length; e < t; e++) l.add(d[e])
        return l
      })
    )
  }
}
class CrossModelTextureCache {
  parser: any
  name: string
  constructor(e) {
    this.parser = e
    this.name = "CrossModelTextureCache"
  }
  static addToLoader(e) {
    e.pluginCallbacks.unshift(e => new CrossModelTextureCache(e))
  }
  loadTexture(e) {
    var t
    const s = this.parser
    const i = s.json
    const r = i.textures[e]
    let o = r.source
    Object.keys(r.extensions).forEach(e => (o = o || r.extensions[e].source))
    const n = null === (t = i.images[o]) || void 0 === t ? void 0 : t.uri
    if (void 0 === n) return null
    const a = b.get(n)
    if (a) return a
    let d = s._invokeOne(t => (t === this ? null : t.loadTexture && t.loadTexture(e)))
    d = d || s.loadTexture(e)
    const l = d.then(
      e => (
        e.addEventListener("dispose", () => {
          b.delete(n)
        }),
        e
      )
    )
    return b.set(n, l), l
  }
}

export default class MttrTileLoader {
  container: any
  chunkFactory: any
  name: string
  geometricErrors: any
  loadStats: {
    startTimes: { start: number; tileset: number; texinfo: number; lod0: number; lod1: number }
    timings: { tileset: string; texinfo: string; lod0: string; lod1: string }
  }
  tilesByLod: {}
  chunksByLod: {}
  onLodLoaded: Map<any, any>
  onChunksLoaded: Set<any>
  minimalLoaded: boolean
  checkLoadStatus: () => void
  tilesRenderer: any
  adjustScreenSpaceError: any
  onModelLoaded: any
  tileset: any
  constructor(e, t, s, i) {
    this.container = e
    this.chunkFactory = i
    this.name = "MttrTileLoader"
    this.geometricErrors = g.t.debugGeometricError.split(",").map(e => Number(e))
    this.loadStats = {
      startTimes: {
        start: 0,
        tileset: 0,
        texinfo: 0,
        lod0: 0,
        lod1: 0
      },
      timings: { tileset: "", texinfo: "", lod0: "", lod1: "" }
    }
    this.tilesByLod = {}
    this.chunksByLod = {}
    this.onLodLoaded = new Map()
    this.onChunksLoaded = new Set()
    this.minimalLoaded = !1
    this.checkLoadStatus = (() => {
      let e = !1,
        t = !1
      return () => {
        ;(e && t) ||
          (e ||
            ((e = this.notifyIfFullyLoaded(0)),
            e &&
              (_.error("LOD0 fully downloaded, allow showing more lods"),
              (this.minimalLoaded = !0),
              (this.loadStats.timings.lod0 = (performance.now() - this.loadStats.startTimes.lod0).toFixed(1) + "ms"),
              (this.loadStats.startTimes.lod1 = performance.now()))),
          t || ((t = this.notifyIfFullyLoaded(1)), t && (this.loadStats.timings.lod1 = (performance.now() - this.loadStats.startTimes.lod1).toFixed(1) + "ms")))
      }
    })()
    this.loadStats.startTimes.start = performance.now()
    this.loadStats.startTimes.texinfo = performance.now()
    const r = t.getSignedUrlSync(`${configConst.tileSet}/${configConst.tilesRoot}`)
    g.t.debug ? (this.tilesRenderer = new DebugTilesRenderer(r)) : (this.tilesRenderer = new TilesRenderer(r))
    const o = (function (e, t) {
      if (!D) {
        D = new DRACOLoader()
        D.setDecoderPath("libs/three@0.128.0/draco/gltf/")
        D.preload()
      }
      const s = new GLTFLoader(t)
      s.setDRACOLoader(D)
      s.register(e => new MTTRMeshBvh(e))
      CrossModelTextureCache.addToLoader(s)
      const i = C(e, t)
      return s.setKTX2Loader(i), s
    })(s, this.tilesRenderer.manager)
    this.configureTilesRenderer(this.tilesRenderer, o), this.configureUrlSigning(this.tilesRenderer, t)
  }
  setSize(e, t, s) {
    this.tilesRenderer.setCamera(e)
    this.tilesRenderer.setResolution(e, t, s)
  }
  notifyOnChunksLoaded(e) {
    return createSubscription(
      () => this.onChunksLoaded.add(e),
      () => this.onChunksLoaded.delete(e),
      !0
    )
  }
  awaitLod(e) {
    const t = this.onLodLoaded.get(e) || []
    const s = new Promise((e, s) => {
      t.push(e)
    })
    this.onLodLoaded.set(e, t)
    return s
  }
  update() {
    this.tilesRenderer instanceof DebugTilesRenderer &&
      ((this.tilesRenderer.displayBoxBounds = g.t.displayBoxBounds), (this.tilesRenderer.colorMode = g.t.colorMode)),
      this.minimalLoaded
        ? ((this.tilesRenderer.maxDepth = g.t.maxDepth + 2),
          (this.tilesRenderer.displayActiveTiles = g.t.displayActiveTiles),
          (this.tilesRenderer.loadSiblings = g.t.loadSiblings),
          (this.tilesRenderer.optimizeRaycast = g.t.optimizeRaycast),
          (this.tilesRenderer.stopAtEmptyTiles = g.t.stopAtEmptyTiles),
          (this.tilesRenderer.autoDisableRendererCulling = g.t.autoDisableRendererCulling))
        : ((this.tilesRenderer.maxDepth = g.t.initialMaxDepth + 2), (this.tilesRenderer.loadSiblings = !0)),
      (this.tilesRenderer.errorTarget = g.t.errorTarget),
      this.tilesRenderer.update(),
      this.checkLoadStatus()
  }
  configureUrlSigning(e, t) {
    e.fetchOptions.mode = "cors"
    e.preprocessURL = e => (e.startsWith("blob") ? e : (e.startsWith("lod") && (e = `${configConst.tileSet}/${e}`), t.getSignedUrlSync(e)))
  }
  configureTilesRenderer(e, t) {
    const s = e.calculateError.bind(e)
    ;(e.calculateError = e => {
      s(e), this.adjustScreenSpaceError && (e.__error = this.adjustScreenSpaceError(e.__error, e))
    }),
      e.manager.addHandler(/\gltf$/, t),
      e.manager.addHandler(/\glb$/, t),
      (e.errorTarget = g.t.errorTarget),
      (e.maxDepth = 1),
      (e.loadSiblings = g.t.loadSiblings),
      (e.stopAtEmptyTiles = g.t.stopAtEmptyTiles),
      e instanceof DebugTilesRenderer && (e.displayBoxBounds = g.t.displayBoxBounds),
      (e.displayActiveTiles = g.t.displayActiveTiles),
      this.container.add(e.group)
    const i = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(-1, 0, 0), THREE.MathUtils.degToRad(90))
    this.container.quaternion.copy(i),
      this.container.updateMatrixWorld(!0),
      (e.onLoadTileSet = this.onLoadTileset.bind(this)),
      (e.onLoadModel = this.onLoadModel.bind(this))
  }
  async onLoadModel(e, t) {
    var s
    const i = (function (e, t, s, i) {
        var r, o, n, d
        const l = [],
          h = (null === (r = t.extras) || void 0 === r ? void 0 : r.floorIndex) || 0,
          u = (null === (o = t.extras) || void 0 === o ? void 0 : o.roomIndex) || 0,
          m = L.get(h, u),
          p = (null === (n = t.extras) || void 0 === n ? void 0 : n.id) || "" + e.id,
          g = (null === (d = t.extras) || void 0 === d ? void 0 : d.textureName) || p
        return (
          (e.name = p),
          e.traverse(e => {
            var r, o, n, d, T
            if (((e.matrixAutoUpdate = !1), e instanceof RoomMesh)) {
              const { group: a, subgroup: h, type: u } = R.xc(e.name),
                L = i(a, h, e.geometry, g)
              ;(e.name = `${e.name}`),
                (e.userData.tileName = p),
                (e.userData.textureName = g),
                (e.userData.tileExtras = t.extras),
                (e.userData.chunks = l),
                (L.textureSizeMax = null === (r = t.extras) || void 0 === r ? void 0 : r.maxTextureSize)
              const x = t.extras
                ? (null !== (o = t.extras.maxTextureSize) && void 0 !== o ? o : 0) * (null !== (n = t.extras.textureScale) && void 0 !== n ? n : 0)
                : 0
              ;(L.textureSizeEmbedded = x > 0 ? x : void 0),
                (L.meshType = u),
                (L.lod = (null === (d = t.extras) || void 0 === d ? void 0 : d.level) || 0),
                (L.name = e.name),
                (L.chunkIndex = (null === (T = t.extras) || void 0 === T ? void 0 : T.chunkIndex) || 0),
                L.notifyOnMaterialUpdated(t => {
                  ;(e.material = t), s && (e[DebugTilesRenderer.ORIGINAL_MATERIAL] = t)
                })
              const f: any = {}
              m && m.getCurrentUniformValues(f),
                (f.map = e.material["map"]),
                (e.material = L.setMaterialsUniform(f)),
                (e.meshGroup = a),
                (e.meshSubgroup = h),
                e.buildWithTileChunk(L),
                l.push(L)
            }
            l.length && L.set(h, u, l[0])
          }),
          l
        )
      })(e, t, g.t.debug, this.chunkFactory),
      r = (null === (s = t.extras) || void 0 === s ? void 0 : s.level) || 0
    this.chunksByLod[r] = (this.chunksByLod[r] || []).concat(i)
    for (const e of i) this.container.addChunk(e)
    for (const e of this.onChunksLoaded.values()) e(i, t)
    this.onModelLoaded && this.onModelLoaded(e)
  }
  onLoadTileset(e) {
    this.tileset = e
    const t = (performance.now() - this.loadStats.startTimes.start).toFixed(1)
    ;(this.loadStats.timings.tileset = t + "ms"),
      _.error(`Tileset info load: ${t}ms`, this.tileset, this.tilesRenderer, this),
      (this.tileset.geometricError = 1 / 0),
      (this.tileset.root.geometricError = 1 / 0),
      this.tilesRenderer.traverse(
        e => {
          if (e.extras && void 0 !== e.extras.level) {
            this.tilesByLod[e.extras.level] = (this.tilesByLod[e.extras.level] || []).concat(e)
            const t = e.extras.level
            e.geometricError = this.geometricErrors[t]
          }
          return !1
        },
        () => !1
      ),
      this.tilesRenderer instanceof DebugTilesRenderer &&
        ((this.tilesRenderer.maxDebugDistance = 300), (this.tilesRenderer.maxDebugError = 10), (this.tilesRenderer.maxDebugDepth = 7)),
      (this.loadStats.startTimes.lod0 = performance.now()),
      this.tilesRenderer.update()
  }
  notifyIfFullyLoaded(e) {
    if (!this.tilesByLod[e]) return !1
    const t = this.tilesByLod[e].filter(e => e.__loadingState !== A)
    if (0 === t.length) for (const t of this.onLodLoaded.get(e) || []) t(this.chunksByLod[e])
    return 0 === t.length
  }
}
